package com.fling.abcde.plugin;

import android.annotation.TargetApi;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;

import com.lib.utils.XLog;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

/**
 * 热修复
 */
final public class HotFix {
    private static final String TAG = HotFix.class.getName();

    /**
     * @param context        上下文对象
     * @param patchDexFile   dex文件路径
     * @param patchClassName patchClassName
     */
    public static void patch(Context context, String patchDexFile, String patchClassName) {

        if (!TextUtils.isEmpty(patchDexFile)) {
            try {
                if (hasLexClassLoader()) {
                    injectInAliYunOs(context, patchDexFile, patchClassName);
                } else if (hasDexClassLoader()) {
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                } else {
                    injectBelowApiLevel14(context, patchDexFile, patchClassName);
                }
            } catch (Error | Exception e) {
                XLog.w("patch" + Log.getStackTraceString(e));
            }
            XLog.i("patch-> ok");
        } else {
            XLog.i("patchDexFile 路径无效");
        }
    }

    private static boolean hasLexClassLoader() {

        try {
            Class.forName("dalvik.system.LexClassLoader");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    private static boolean hasDexClassLoader() {

        try {
            Class.forName("dalvik.system.BaseDexClassLoader");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    private static void injectInAliYunOs(Context context, String patchDexFile, String patchClassName) throws ClassNotFoundException, NoSuchMethodException,
            IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {

        PathClassLoader obj = (PathClassLoader) context.getClassLoader();
        String str = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
        Class<?> cls = Class.forName("dalvik.system.LexClassLoader");
        String dex = context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath();
        String tmp = context.getDir("tmp", Context.MODE_PRIVATE).getAbsolutePath();
        Object[] initArgs = {dex + File.separator + str, tmp, patchDexFile, obj};
        Object newInstance = cls.getConstructor(new Class[] {String.class, String.class, String.class, ClassLoader.class}).newInstance(initArgs);

        if (!TextUtils.isEmpty(patchClassName)) {
            cls.getMethod("loadClass", new Class[] {String.class}).invoke(newInstance, patchClassName);
        }

        setField(obj, PathClassLoader.class, "mPaths",
                appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
        setField(obj, PathClassLoader.class, "mFiles",
                combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
        setField(obj, PathClassLoader.class, "mZips",
                combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
        setField(obj, PathClassLoader.class, "mLexs",
                combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
    }

    private static void injectAboveEqualApiLevel14(Context context, String patchDexFile, String patchClassName) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        String tmpPath = context.getDir("tmp", Context.MODE_PRIVATE).getAbsolutePath();
        DexClassLoader dexClassLoader = new DexClassLoader(patchDexFile, tmpPath, patchDexFile, context.getClassLoader());
        Object a = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
        Object a2 = getPathList(pathClassLoader);
        //新的dexElements对象重新设置回去
        setField(a2, a2.getClass(), "dexElements", a);

        if (!TextUtils.isEmpty(patchClassName)) {
            pathClassLoader.loadClass(patchClassName);
        }
    }

    @TargetApi(14)
    private static void injectBelowApiLevel14(Context context, String patchDexFile, String patchClassName) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

        PathClassLoader obj = (PathClassLoader) context.getClassLoader();
        String tmp = context.getDir("tmp", Context.MODE_PRIVATE).getAbsolutePath();
        DexClassLoader dexClassLoader = new DexClassLoader(patchDexFile, tmp, patchDexFile, context.getClassLoader());

        if (!TextUtils.isEmpty(patchClassName)) {
            dexClassLoader.loadClass(patchClassName);
        }
        setField(obj, PathClassLoader.class, "mPaths",
                appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class, "mRawDexPath")));
        setField(obj, PathClassLoader.class, "mFiles",
                combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class, "mFiles")));
        setField(obj, PathClassLoader.class, "mZips",
                combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class, "mZips")));
        setField(obj, PathClassLoader.class, "mDexs",
                combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class, "mDexs")));

        if (!TextUtils.isEmpty(patchClassName)) {
            obj.loadClass(patchClassName);
        }
    }

    /**
     * 通过反射先获取到pathList对象
     *
     * @param obj
     *
     * @return
     *
     * @throws ClassNotFoundException
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 从上面获取到的PathList对象中，进一步反射获得dexElements对象
     *
     * @param obj
     *
     * @return
     *
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {

        return getField(obj, obj.getClass(), "dexElements");
    }

    private static Object getField(Object obj, Class<?> cls, String str) throws NoSuchFieldException, IllegalAccessException {

        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);//设置为可访问
        return declaredField.get(obj);
    }

    private static void setField(Object obj, Class<?> cls, String str, Object obj2) throws NoSuchFieldException, IllegalAccessException {

        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);//设置为可访问
        declaredField.set(obj, obj2);
    }

    //合拼dexElements
    private static Object combineArray(Object obj, Object obj2) {

        Class<?> componentType = obj2.getClass().getComponentType();
        int length = Array.getLength(obj2);
        int length2 = Array.getLength(obj) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) {
            if (i < length) {
                Array.set(newInstance, i, Array.get(obj2, i));
            } else {
                Array.set(newInstance, i, Array.get(obj, i - length));
            }
        }
        return newInstance;
    }

    private static Object appendArray(Object obj, Object obj2) {

        Class<?> componentType = obj.getClass().getComponentType();
        int length = Array.getLength(obj);
        Object newInstance = Array.newInstance(componentType, length + 1);
        Array.set(newInstance, 0, obj2);
        for (int i = 1; i < length + 1; i++) {
            Array.set(newInstance, i, Array.get(obj, i - 1));
        }
        return newInstance;
    }
}